<?php
namespace Tlf\User;
class Lib {
use Throttle;
/**
* Expects `web_address = http://example.com`
* Expects `email_from = help@example.com`
*
*/
public $config = [];
/** valid symbol characters for a password */
public string $password_symbols = '~`!@#$%^&*()_-+={[]}|\\:;"\'<,>.?/';
/** max len should not be changed, bc bcrypt limit is 72 chars */
public int $max_len = 72;
public int $min_len = 8;
public bool $require_num = true;
public bool $require_symbol = true;
public bool $require_mix_case = true;
/** the class of the user to instantiate */
public string $user_class = '\\Tlf\\User';
public \PDO $pdo;
/**
* to disable a page just add its page identifier to this array. One of 'login', 'register', 'reset-password', 'logout', or 'terms'
*/
public $disabled_pages = [];
/**
* This should only be set after a session is validated
* @key the csrf token name
* @value true, always true
*/
public array $valid_sessions = [];
/**
*
* @key the key_prefix
* @value the actual key (with uniqid)
*/
public array $latest_csrf = [];
/**
* Array of query strings identifiable by key. Generated by LilSql (of LilDb package)
*/
public array $queries = [];
public function __construct($pdo){
$this->pdo = $pdo;
$this->queries = unserialize(file_get_contents(__DIR__.'/../db/serialized.txt'));
}
public function is_post():bool{
if ($_SERVER['REQUEST_METHOD']=='POST')return true;
return false;
}
public function init_db(){
$pdo = $this->pdo;
$errmode = $pdo->getAttribute(\PDO::ATTR_ERRMODE);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$sql = file_get_contents(dirname(__DIR__).'/db/create.sql');
$pdo->exec($sql);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, $errmode);
// if ($pdo->errorCode()!='00000'){
// print_r($err);
// throw new \PDOException()
// }
}
/**
* Checks if a page is disabled in `$this->disabled_pages` and outputs a message if so
*
* @output a message that the page is disabled
* @return true/false
*/
public function page_is_disabled(string $page_id){
// print_r($this->disabled_pages);
// var_dump($page_id);
// exit;
if (!in_array($page_id, $this->disabled_pages))return false;
if (!headers_sent()){
header('HTTP/1.1 403 Forbidden', 403);
}
echo "\n<h1>Page Disabled</h1>\n<p>You are not allowed to access this page</p>\n";
return true;
// $url = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// $parts = explode('/',$url);
// $size = count($parts);
// if ($parts[$size-1]=='')$size--;
// $rel_url = '/'.$parts[$size-1];
// if (!in_array($rel_url, $this->disabled_pages)){
// return true;
// }
//
// echo "\n<h1>Page Disabled</h1>\n<p>You are not allowed to access this page</p>\n";
//
// return false;
}
/**
* delete an entry in `role_permission` table
*/
public function role_deny(string $role, string $permission){
$stmt = $this->pdo->prepare($this->queries['role.deny']);
$stmt->execute(['role'=>$role, 'permission'=>$permission]);
}
/**
* Delete all entries with given role from `role_permission` and `user_role` tables
*/
public function role_delete(string $role){
$stmt = $this->pdo->prepare($this->queries['role.delete']);
$stmt->execute(['role'=>$role]);
// var_dump($stmt->rowCount());
// print_r($this->pdo->errorInfo());
// exit;
}
/**
* add entry to `role_permission` table
*/
public function role_allow(string $role, string $permission){
$stmt = $this->pdo->prepare($this->queries['role.allow']);
$stmt->execute(['role'=>$role, 'permission'=>$permission]);
// var_dump($stmt->rowCount());
// print_r($this->pdo->errorInfo());
// exit;
}
/**
* @return true/false whether password meets requirements or not
*/
public function is_password_valid(string $password){
$len = strlen($password);
if ($len < $this->min_len || $len > $this->max_len)return false;
if ($this->require_num&&!preg_match('/\d/', $password))return false;
if ($this->require_mix_case&&
(!preg_match('/[a-z]/', $password) ||
!preg_match('/[A-Z]/', $password)
)
) return false;
// echo 'len & digits';
// exit;
$symbols = $this->password_symbols;
$reg = preg_quote($symbols, '/');
if ($this->require_symbol&&!preg_match("/[$reg]/", $password))return false;
return true;
}
/**
* Get an array of users.
*
* @param string $role
* @return array of user rows (or objects if 2nd param is true)
*/
public function users_with_role(string $role): array {
$class = $this->user_class;
//$user = new $class($this->pdo);
//$user->email = $email;
$pdo = $this->pdo;
$stmt = $pdo->prepare($this->queries['user.with_role']);
$stmt->execute(['role'=>$role]);
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
return $rows;
}
/**
* get a user by their email. User may or may not be in the database/registered/active
* @return a user object (always, regardless of user existing in database)
*/
public function user_from_email(string $email){
$class = $this->user_class;
$user = new $class($this->pdo);
$user->email = $email;
$pdo = $this->pdo;
$stmt = $pdo->prepare($this->queries['user.from_email']);
$stmt->execute(['email'=>$email]);
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
if (count($rows)!==1)return $user;
$user->from_row($rows[0]);
return $user;
}
/**
* Login a user by validating the cookie sent with their request
* @note for testing set $_COOKIE['taeluf_login'] = $code prior to calling this function
* @param $cookie optional cookie code to use. else uses $_COOKIE['taeluf_login']
*
* @return user object if succesful, false otherwise
*/
public function user_from_cookie($cookie=null) {
// $code = $_COOKIE[$this->cookie_name] ?? null;
$code = $cookie ?? $_COOKIE[\Tlf\User::$cookie_name]??null;
if ($code==null)return false;
$stmt = $this->pdo->prepare($this->queries['user.from_cookie']);
$stmt->execute(['code'=>$code]);
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
if (count($rows)!==1)return false;
$row = $rows[0];
if ($row['is_active']!=true)return false;
$class = $this->user_class;
$user = new $class($this->pdo);
$user->email = $row['email'];
$user->is_logged_in = true;
$user->id = $row['id'];
return $user;
}
////////////
// csrf
////////////
public function make_csrf_code(){
// this code from symfony csrf package: https://github.com/symfony/security-csrf/blob/5.4/TokenGenerator/UriSafeTokenGenerator.php
$bytes = random_bytes(64);
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}
/**
*
*
* @param $key_prefix string to help identify your csrf token.
* @param $expiry_minutes number of minutes the token should be valid for
* @param $url the url path the token should be validated on, like '/some/url/'. If not set, it works on any path
*
* @return the csrf key. To load csrf data do `$_SESSION[$csrf_key]`. `$csrf_key` will be like `key_prefix-csrf-uniqid()`
*/
public function enable_csrf(string $key_prefix='',int $expiry_minutes=60, string $url_path=''){
$key = $key_prefix.'-csrf-'.uniqid();
$data = [
'code'=> $this->make_csrf_code(),
'expires_at' => time() + $expiry_minutes * 60,
'uri' => $url_path,
];
if (session_status()==PHP_SESSION_NONE)session_start();
if (session_status()!=PHP_SESSION_ACTIVE)throw new \Exception("Failed to start session. Cannot do csrf without session.");
$_SESSION[$key] = $data;
$this->latest_csrf[$key_prefix] = $key;
// error_log('csrf key: '.$key);
return $key;
}
/**
* get the key of the csrf data in `$_POST` for the given key
* @param $key_prefix see csrf_is_valid
*/
public function get_csrf_post_key(string $key_prefix=''): string {
$len = strlen($key_prefix) + strlen('-csrf-');
foreach ($_POST as $key=>$value){
if (substr($key,0,$len)!=$key_prefix.'-csrf-')continue;
$post_key = $key;
// $post_code = $value;
return $post_key;
// break;
}
return '';
}
public function get_csrf_session_key(string $key_prefix=''): string {
if (isset($this->latest_csrf[$key_prefix]))return $this->latest_csrf[$key_prefix];
$len = strlen($key_prefix) + strlen('-csrf-');
foreach ($_SESSION as $key=>$value){
if (substr($key,0,$len)!=$key_prefix.'-csrf-')continue;
return $key;
}
return '';
}
public function get_csrf_session_input(string $key_prefix=''): string {
$key = $this->get_csrf_session_key($key_prefix);
$code = $_SESSION[$key]['code'];
return '<input type="hidden" name="'.$key.'" value="'.$code.'">';
}
/**
* Checks `$_POST` for the csrf token
*
* @param $key_prefix the same key prefix you passed to `$this->enable_csrf()`
* @return true/false
*/
public function csrf_is_valid(string $key_prefix=''): bool {
// this attempts to do the checks listed on https://www.taeluf.com/blog/php/security/csrf-validation/
$post_key = $this->get_csrf_post_key($key_prefix);
if ($post_key=='')return false;
$post_code = $_POST[$post_key];
// because i unset from $_SESSION
if (isset($this->valid_sessions[$post_key]))return true;
if (session_status()==PHP_SESSION_NONE)session_start();
if (session_status()!=PHP_SESSION_ACTIVE)throw new \Exception("Failed to start session. Cannot do csrf without session.");
if (!isset($_SESSION[$post_key]))return false;
$session_csrf = $_SESSION[$post_key];
if ($session_csrf['code'] != $post_code) return false;
if ($session_csrf['expires_at'] < time()) return false;
$post_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($session_csrf['uri'] != ''
&&$session_csrf['uri'] != $post_path
)return false;
if (!isset($_SERVER['HTTP_REFERER']))return false;
$referer_domain = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
// to remove the port (mainly bc of localhost testing)
$server_host = parse_url($_SERVER['HTTP_HOST'], PHP_URL_HOST);
if ($server_host==null)$server_host = $_SERVER['HTTP_HOST'];
if ($referer_domain != $server_host)return false;
unset($_SESSION[$post_key]);
$this->valid_sessions[$post_key] = true;
return true;
}
public function security_consent_box(){
echo "
<label>
Your IP address and browser's user agent will be logged with the submitted email address & will be viewable by the owner of the account & website administators.<br>
<input type=\"checkbox\" name=\"logs_consent\" required>
I consent to security logging<br>
</label>
";
}
}